//////////////////////////////////////////////
// PastCode.h
//
// Some code from last tutorials used
// to initialize and clean a bit the main
// file
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkGraphics
#include <NilkinsGraphics/Cameras/Camera.h>
#include <NilkinsGraphics/Cameras/CameraManager.h>

#include <NilkinsGraphics/Encoders/Obj/ObjEncoder.h>

#include <NilkinsGraphics/Entities/Entity.h>

#include <NilkinsGraphics/Graph/Node.h>
#include <NilkinsGraphics/Graph/NodeManager.h>

#include <NilkinsGraphics/Log/LogManager.h>

#include <NilkinsGraphics/Meshes/Utils/MeshUtils.h>

#include <NilkinsGraphics/Meshes/Mesh.h>
#include <NilkinsGraphics/Meshes/MeshManager.h>

#include <NilkinsGraphics/Programs/Program.h>
#include <NilkinsGraphics/Programs/ProgramManager.h>
#include <NilkinsGraphics/Programs/ProgramSourcesHolder.h>

#include <NilkinsGraphics/RenderQueues/RenderQueue.h>
#include <NilkinsGraphics/RenderQueues/RenderQueueManager.h>

#include <NilkinsGraphics/Samplers/Sampler.h>
#include <NilkinsGraphics/Samplers/SamplerManager.h>

#include <NilkinsGraphics/Shaders/Memory/ConstantBuffer.h>
#include <NilkinsGraphics/Shaders/Memory/ShaderInstanceMemorySlot.h>
#include <NilkinsGraphics/Shaders/Memory/ShaderPassMemorySlot.h>

#include <NilkinsGraphics/Shaders/Shader.h>
#include <NilkinsGraphics/Shaders/ShaderManager.h>

#include <NilkinsGraphics/System.h>

#include <NilkinsGraphics/Textures/Texture.h>
#include <NilkinsGraphics/Textures/TextureManager.h>

// nkMemory
//#include <NilkinsMemory/Containers/Buffer.h>
//#include <NilkinsMemory/Containers/String.h>

// nkResources
#include <NilkinsResources/ResourceManager.h>

// nkWinUi
#include <NilkinsWinUi/System.h>

// Standards
#include <chrono>

#include <memory>

/// Internals : Mesh -------------------------

void prepareMeshFromFile ()
{
	// Initialize mesh to use later
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// Load data from file
	nkMemory::String absPath = nkResources::ResourceManager::getInstance()->getAbsoluteFromWorkingDir("sphere.obj") ;
	nkMemory::Buffer objData = nkResources::ResourceManager::getInstance()->loadFileIntoMemory(absPath) ;

	// We change some settings to alter the way the mesh is imported
	nkGraphics::ObjDecodeOptions objOptions ;
	objOptions._invertUvY = true ;
	objOptions._invertWindingOrder = true ;
	nkGraphics::DecodedData objDecoded = nkGraphics::ObjEncoder::decode(objData, objOptions) ;

	// Fill mesh
	nkGraphics::MeshFillOptions fillOptions ;
	fillOptions._autoLoad = true ;
	fillOptions._dataFillType = nkGraphics::DATA_FILL_TYPE::FORWARD ;
	nkGraphics::MeshUtils::fillMeshFromDecodedData(objDecoded._meshData[0], mesh, fillOptions) ;
}

void addMeshToRenderQueue ()
{
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(0) ;

	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	nkGraphics::Entity* ent = rq->addEntity() ;
	ent->setRenderInfo(nkGraphics::EntityRenderInfo(mesh, nullptr)) ;
}

void addEntityToGraph ()
{
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(0) ;
	nkGraphics::Entity* ent = rq->getEntity(0) ;

	nkGraphics::Node* node = nkGraphics::NodeManager::getInstance()->createOrRetrieve("node") ;
	node->setPositionAbsolute(nkMaths::Vector(0.f, 0.f, 10.f)) ;

	ent->setParentNode(node) ;
}

/// Internals : Texture ----------------------

void prepareTexture ()
{
	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->createOrRetrieve("tex") ;

	tex->setPath("yokohamaNight.dds") ;
	tex->setGammaCorrected(false) ;
	tex->load() ;
}

void prepareSampler ()
{
	nkGraphics::Sampler* sampler = nkGraphics::SamplerManager::getInstance()->createOrRetrieve("sampler") ;
	sampler->load() ;
}

/// Internals : Shader -----------------------

void prepareSphereProgram ()
{
	// Our program will be very similar to last time : only need to sample a cubemap through the right class and direction
	nkGraphics::Program* program = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("program") ;
	nkGraphics::ProgramSourcesHolder sources ;

	sources.setVertexMemory
	(
		R"eos(
			cbuffer PassBuffer : register(b0)
			{
				matrix view ;
				matrix proj ;
				float3 camPos ;
			}

			struct VertexInput
			{
				float4 position : POSITION ;
				float3 normal : NORMAL ;
				matrix world : WORLD_MATRIX ;
			} ;

			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float3 normal : NORMAL ;
				float3 camDir : CAMDIR ;
			} ;

			PixelInput main (VertexInput input)
			{
				PixelInput result ;

				matrix mvp = mul(proj, mul(view, input.world)) ;
				result.position = mul(mvp, input.position) ;
				result.normal = input.normal ;
				result.camDir = normalize(mul(input.world, input.position) - camPos) ;

				return result ;
			}
		)eos"
	) ;

	sources.setPixelMemory
	(
		R"eos(
			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float3 normal : NORMAL ;
				float3 camDir : CAMDIR ;
			} ;

			TextureCube tex : register(t0) ;
			SamplerState customSampler : register(s0) ;
				
			float4 main (PixelInput input) : SV_TARGET
			{
				float3 sampleDir = reflect(input.camDir, normalize(input.normal)) ;
				return tex.Sample(customSampler, sampleDir) ;
			}
		)eos"
	) ;

	program->setFromMemory(sources) ;
	program->load() ;
}

void prepareEnvProgram ()
{
	// This program will be used to render a background in sync with the sphere rendered
	nkGraphics::Program* envProgram = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("envProgram") ;
	nkGraphics::ProgramSourcesHolder envSources ;

	envSources.setVertexMemory
	(
		R"eos(
			// The camDir entry is an array of 4 float4
			// They will correspond to the 4 corners of the screen
			// This is true because we will use this shader in a post process pass
			// Such a pass map a quad to the screen and renders it using the shader provided
			cbuffer constants
			{
				float4 camDir [4] ;
			}

			// The vertexId is fed by DirectX and correspond to the index of the vertex being processed
			// A pass slot for camera direction feeds the corners in sync with the order the vertices are put in the mesh
			// As such, we can use it to directly index which direction a vertex corresponds to
			struct VertexInput
			{
				float4 position : POSITION ;
				uint vertexId : SV_VertexID ;
			} ;

			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float4 camDir : CAMDIR ;
			} ;

			PixelInput main (VertexInput input)
			{
				PixelInput result ;

				result.position = input.position ;
				result.camDir = camDir[input.vertexId] ;

				return result ;
			}
		)eos"
	) ;

	envSources.setPixelMemory
	(
		R"eos(
			// The camera direction will be interpolated between the 4 vertices, for each pixel
			// This means we will get the direction for a given pixel in the world
			// Using that knowledge we only need to sample the cube map
			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float4 camDir : CAMDIR ;
			} ;

			TextureCube envMap : register(t0) ;
			SamplerState customSampler : register(s0) ;

			float4 main (PixelInput input) : SV_TARGET
			{
				return envMap.Sample(customSampler, normalize(input.camDir)) ;
			}
		)eos"
	) ;

	envProgram->setFromMemory(envSources) ;
	envProgram->load() ;
}

void prepareSphereShader ()
{
	// Prepare the shader which will be used by the sphere
	nkGraphics::Shader* shader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("shader") ;
	nkGraphics::Program* program = nkGraphics::ProgramManager::getInstance()->get("program") ;

	shader->setProgram(program) ;

	nkGraphics::ConstantBuffer* cBuffer = shader->addConstantBuffer(0) ;

	nkGraphics::ShaderPassMemorySlot* slot = cBuffer->addPassMemorySlot() ;
	slot->setAsViewMatrix() ;

	slot = cBuffer->addPassMemorySlot() ;
	slot->setAsProjectionMatrix() ;

	slot = cBuffer->addPassMemorySlot() ;
	slot->setAsCameraPosition() ;

	nkGraphics::ShaderInstanceMemorySlot* instanceSlot = shader->addInstanceMemorySlot() ;
	instanceSlot->setAsWorldMatrix() ;

	// Prepare for tex and sampler
	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->get("tex") ;
	nkGraphics::Sampler* sampler = nkGraphics::SamplerManager::getInstance()->get("sampler") ;

	shader->addTexture(tex, 0) ;
	shader->addSampler(sampler, 0) ;

	shader->load() ;
}

void prepareEnvShader ()
{
	// Prepare the post process shader
	nkGraphics::Program* envProgram = nkGraphics::ProgramManager::getInstance()->get("envProgram") ;

	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->get("tex") ;
	nkGraphics::Sampler* sampler = nkGraphics::SamplerManager::getInstance()->get("sampler") ;

	nkGraphics::Shader* envShader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("envShader") ;
	
	envShader->setProgram(envProgram) ;

	nkGraphics::ConstantBuffer* cBuffer = envShader->addConstantBuffer(0) ;

	nkGraphics::ShaderPassMemorySlot* slot = cBuffer->addPassMemorySlot() ;
	slot->setAsCamCornersWorld() ;

	envShader->addTexture(tex, 0) ;
	envShader->addSampler(sampler, 0) ;

	envShader->load() ;
}

void assignShader ()
{
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;
	nkGraphics::Entity* ent = rq->getEntity(0) ;
	nkGraphics::Shader* shader = nkGraphics::ShaderManager::getInstance()->get("shader") ;

	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;
	ent->setRenderInfo(nkGraphics::EntityRenderInfo(mesh, shader)) ;
}

/// Internals : Base -------------------------

void baseInit ()
{
	// Prepare the mesh we will show
	prepareMeshFromFile() ;
	addMeshToRenderQueue() ;
	addEntityToGraph() ;

	// Prepare the texture we will use
	prepareTexture() ;
	prepareSampler() ;

	// Change the sphere's shader
	prepareSphereProgram() ;
	prepareSphereShader() ;
	assignShader() ;
	// Load the post processing ones
	prepareEnvProgram() ;
	prepareEnvShader() ;
}

void renderLoop (nkGraphics::RenderContext* context)
{
	// Get some data to prepare for the rendering loop
	nkGraphics::Camera* camera = nkGraphics::CameraManager::getInstance()->getDefaultCamera() ;

	nkGraphics::System* nkGraphicsSystem = nkGraphics::System::getInstance() ;
	nkWinUi::System* nkWinUiSystem = nkGraphicsSystem->getUiSystem() ;

	const float loopTimeMs = 5 * 1000.f ;
	std::chrono::system_clock::time_point firstNow = std::chrono::system_clock::now() ;

	const nkMaths::Vector sphereCenter = nkMaths::Vector(0.f, 0.f, 10.f) ;

	// Our rendering loop is ready to start
	while (nkGraphicsSystem->getHasRunToContinue())
	{
		// Frame the graphics system
		if (!nkGraphicsSystem->frame(context))
			break ;

		// Loop the camera around the sphere
		std::chrono::system_clock::time_point now = std::chrono::system_clock::now() ;
		float currentTimeMs = std::chrono::duration_cast<std::chrono::microseconds>(now - firstNow).count() / 1000.f ;
		
		float currentLoopT = (std::fmod(currentTimeMs, loopTimeMs) / loopTimeMs) * 2.f - 1.f ;
		currentLoopT *= 3.14 ;
		
		nkMaths::Vector loopPos (std::cos(currentLoopT) * 10.f, 0.f, std::sin(currentLoopT) * 10.f) ;
		loopPos += sphereCenter ;

		camera->setPositionAbsolute(loopPos) ;
		camera->lookAt(sphereCenter, nkMaths::Vector(0.f, 1.f, 0.f)) ;

		// Tick the windowing system
		nkWinUiSystem->tick() ;
	}
}